iT邦幫忙

2024 iThome 鐵人賽

DAY 12
2
Software Development

可以Go一輩子嗎?系列 第 12

Day12. 結構化程式設計(1)

  • 分享至 

  • xImage
  •  

Day12. 結構化程式設計(1)

介紹

相信寫過Python, Java跟CSharp的人都知道物件導向的概念,不過Go語言本身並不完全是一個物件導向的程式語言。
在官方的FAQ也有提到,即使Go語言可以作到OOP的行為,但Go語言沒有Class也沒有物件的概念,而是透過struct與interface來實現。且官方也明確說到Go語言不支援繼承

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

  • 物件導向的差異
  1. Go語言沒有型態上的階層關係,而是透過類型嵌入(Type embedding)作到類似繼承的行為
  2. Go語言本身透過interface來實現多型(Polymorphism),而不是透過繼承
  3. Go語言中的method本質上是一個function,也就是說除了可以綁定給其他類別,也可以直接呼叫。且Go語言沒有專門的init函數(如python的__init__)

Struct

定義

使用方式與C 類似,假設我們要定義一個人的struct

type Human struct{
    name, mood string
    age, height int
}

然後撰寫初始化function

// struct as args
func NewHuman(name, mood string, age, height int) Human {
    return Human{
        name: name,
        age: age,
        mood: mood,
        height: height,
    }
}

再來在需要使用的地方呼叫NewHuman並設定參數就行了

新增method

前面提到Go語言中的method本質上是一個function。所以我們能透過綁定function給這個struct以指定該function做為method,這邊我拿go tour的method範例code來舉例

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

透過上面的定義,我們成功將AbsScale綁定給Vertex這個struct,不過眼尖的人應該會注意到剛才的範例的兩個method在接受struct的方式有點不太一樣。這種綁定pointer的method在Go語言中叫做Pointer receivers,在method中如果有修改任意屬性的話,在離開method後變更也會應用到那個被呼叫的struct

func (h *Human) ChangeName(name string){
    h.name = name
}
  • 值得注意的是,Go語言允許你透過指標直接存取struct,因此即使你在receiver指定的是struct pointer, 也不需要特別dereference。

Struct Tag

我們可以透過指定tag來定義struct的屬性,在後續的操作中更方便的取得struct的屬性
struct tag 主要用於序列化與反序列化(如json, xml, gob等),也可以用於資料庫ORM(Object Relational Mapping)的映射

每個tag都是一個key-value pair,key是tag的名稱,value是tag的值。定義Tag時使用反引號(`)包裹,可以避免轉義字元的問題,多個tag之間使用空格分隔,但value中不建議使用空格,以免因為空格分隔而造成解析錯誤

`key1:"value1" key2:"value2"`

假設我們目前有一個json

{
    "name": "human",
    "mood": "neutral",
    "age": 16,
    "height": 162
}

我們可以透過tag來定義struct的屬性,然後透過json.Unmarshal來將json轉換成struct

import (
    "encoding/json"
    "fmt"
)
type Human struct{
    Name string `json:"name"`
    Mood string `json:"mood"`
    Age int `json:"age"`
    Height int `json:"height"`
}
func main() {
    jsonStr := `{
        "name": "human",
        "mood": "neutral",
        "age": 16,
        "height": 162
    }`
    human := Human{}

    // 將human變數的值設定為轉換的struct
    err := json.Unmarshal([]byte(jsonStr), &human)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Printf("%+v\n", human)
}

類型嵌入(Type embedding)

前面提到了Go語言透過類型嵌入(Type embedding)實現繼承的功能,只需要將你的Base struct作為屬性(attribute)加入到新的struct就行(注意,被繼承的struct不可在新struct中定義類別, 除非你想要定義nested struct)

  • 假設我現在要透過Human繼承到AdultChildren
type Adult struct {
    Human
    jobs string
    income int
}

type Children{
    Human
    grade int
}

接著透過以下方式進行初始化

func NewAdult(name, mood, jobs string, age, height, income int) Adult {
    baseHuman := Human{
        name: name,
        age: age,
        mood: mood,
        height: height
    }
    return Adult{
        Human: baseHuman,
        jobs: jobs,
        income: income
    }
}

假如你想對特定的物件單獨設定method的話,可以透過嵌入Base struct來針對新的struct綁定method
PS: 由於這次是針對特定物件來繼承,所以需要使用&{}宣告以獲取記憶體位置,確保修改到struct

type Soyo struct{ // 這種方式其實我不太確定該怎麼稱呼
    Human
}

func (soyo *Soyo) Kneeling() {
	soyo.mood = "Sad"
	soyo.SaySomething("要是沒有小祥你們的話,我就...\n要我怎麼做你們才肯回來? 😭只要是我能做的,我什麼都願意做")
}

func main() {
	soyo := &Soyo{
		Human: NewHuman("soyo", "", 16, 162),
	}
	soyo.Kneeling()
}

image

在Go語言實現封裝

之前的文章有提到屬性是透過首字母表示公開(public)或私有(private),因此只要在設定屬性時將首字母設為小寫就可以達成封裝的效果(也就是除非在同一package,否則無法在外部更改該屬性,只能透過綁定Getter與Setter來對屬性進行訪問與修改)

func (h *Human) GetMood() string{
    return h.mood
}

func main() {
	soyo := &Soyo{
		Human: NewHuman("soyo", "", 16, 162),
	}
	soyo.Kneeling()
    // fmt.Println(soyo.mood) 此行會造成錯誤
    fmt.Println(soyo.GetMood()) 此行會造成錯誤
}

那麼今天的文章就到此告一段落,如果我的文章有任何地方有錯誤請在留言區反應。
明天將會繼續講在Go語言中如何作到類似物件導向的行為,並且講如何使用Interface實現多型態
time

REF


上一篇
Day11. 檔案處理
下一篇
Day13. 結構化程式設計(2)
系列文
可以Go一輩子嗎?31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言